// @Creation:	30.04.2012
// @Version:	30.05.2012
// 
// @cpu:	x86,
// @author:	alexey kurshakov [alexey.kurshakov@gmail.com]
//
// @Contents:	serialportRS232.go 
//	Функции для работы с последовательным портом
//	Реализация интерфейса для работы с COM-портом
//

package serialport

/*
#include "serialport.h"
*/
import "C"

import (
	"bytes"
	"io"
	"time"
	"unsafe"
)

// Перечисление скоростей доступных для RS-232
const (
	BaudRate2400   uint = 2400
	BaudRate4800   uint = 4800
	BaudRate9600   uint = 9600
	BaudRate19200  uint = 19200
	BaudRate38400  uint = 38400
	BaudRate57600  uint = 57600
	BaudRate115200 uint = 115200
)

// Перечисление паритетов для RS-232
const (
	ParityNone  string = "NOPARITY" // нет паритета
	ParityEven  string = "EVEN"     // четный паритет
	ParityOdd   string = "ODD"      // нечетный паритет
	ParityMark  string = "MARK"     //
	ParitySpace string = "SPACE"    //
)

// Перечисление размера байта для передачи по RS-232
const (
	ByteSize4 byte = 4
	ByteSize5 byte = 5
	ByteSize6 byte = 6
	ByteSize7 byte = 7
	ByteSize8 byte = 8
)

// Перечисление возможных значений стоп-битов
const (
	OneStopBit     byte = 0 // 1 стоп-бит
	OneHalfStopBit byte = 1 // 1,5 стоп-бита
	TwoStopBit     byte = 2 // 2 стоп-бита
)

// Структура, описывающая настройки
// для работы с COM-портом
type COMPortSettings struct {
	DevName     string // имя используемого устройства
	BaudRate    uint   // скорость работы порта
	Parity      string // установленный паритет
	ByteSize    byte   // размер байта
	StopBits    byte   // количество стоп-битов
	IOTimeOutMs uint   // таймаут на io-операции, в мс
}

// объект - последовательный COM-порт
type COMPort struct {
	rMutex   chan int         // мьютекс, разделяющий операции чтения
	wMutex   chan int         // мьютекс, разделяющий операции записи
	rBuffer  bytes.Buffer     // данные, накопленные в приемнике COM-порта
	settings *COMPortSettings // текущий настройки

	waitCondMutex     chan int // условие рассылки для ожидающих данных в порту
	waitCondLockMutex chan int // мьютекс для поддержки waitCondMutex
	waitReadersNum    int      // количество ожидающих

	unknownData unsafe.Pointer // указатель на данные, используемые в C-части
}

// Оповешаем всех ожидаюших о появлении новых данные в порту
func sendSignalForWaiters(port *COMPort) {
	port.waitCondLockMutex <- 1
	for ; port.waitReadersNum > 0; port.waitReadersNum-- {
		port.waitCondMutex <- 1
	}
	<-port.waitCondLockMutex
}

// Ждем пока освободится канал для его захвата
func waitForMutex(c chan int, timeOutMs uint) error {
	select {
	case c <- 1:
	case <-time.After(time.Duration(timeOutMs) * time.Millisecond):
		return IOTimeOut
	}
	return NoError
}

//export writeDataToGoCOMPort
func writeDataToGoCOMPort(COMPortPtr unsafe.Pointer, data unsafe.Pointer, dataSize C.int) {
	port := (*COMPort)(COMPortPtr)
	port.rBuffer.Write(C.GoBytes(data, dataSize))
	// Оповещаем всех ожидающих, что есть новые данные
	sendSignalForWaiters(port)
}

// Создать новый экземпляр настроек для работы с COM-портом
func NewCOMPortSettings() *COMPortSettings {
	settings := new(COMPortSettings)

	settings.BaudRate = BaudRate9600
	settings.Parity = ParityNone
	settings.ByteSize = ByteSize8
	settings.StopBits = OneStopBit
	settings.IOTimeOutMs = 500

	return settings
}

// Преобразуем ошибки С-части в ошибки принятые в Go
func cerror2GoError(errorRes C.int) error {
	switch errorRes {
	case C.eNoError:
		return NoError
	case C.eUnknownDevice:
		return UnknownDevice
	case C.eIOTimeOut:
		return IOTimeOut
	case C.eIOError:
		return IOError
	case C.eFail:
		return Failed
	default:
		print("unknown error ", errorRes, " skipped, but...\n")
	}
	return NoError
}

// Открыть для работы COM-порт
func (s *COMPortSettings) Open() (port *COMPort, err error) {
	// 1. Создаем экземпляр настроек и конвертим настройки go в C
	var c_settings C.COMPortSettings
	c_settings.devName = C.CString(s.DevName)
	defer C.free(unsafe.Pointer(c_settings.devName))
	c_settings.baudRate = C.uint(s.BaudRate)
	switch s.Parity {
	case ParityNone:
		c_settings.parity = C.eParityNone
	case ParityEven:
		c_settings.parity = C.eParityEven
	case ParityOdd:
		c_settings.parity = C.eParityOdd
	case ParityMark:
		c_settings.parity = C.eParityMark
	case ParitySpace:
		c_settings.parity = C.eParitySpace
	}
	c_settings.byteSize = C.char(s.ByteSize)
	c_settings.stopBits = C.char(s.StopBits)
	c_settings.ioTimeOutMs = C.uint(s.IOTimeOutMs)

	// 2. Открываем порт и обрабатываем результат
	port = new(COMPort)
	err = cerror2GoError(C.int(
		C.OpenCOMPort(&c_settings, unsafe.Pointer(port), &(port.unknownData))))
	if err != NoError {
		port = nil
		return
	}

	// 3.Инициализируем переменные порта
	port.rMutex = make(chan int, 1) // }
	port.wMutex = make(chan int, 1) // }
	port.waitCondMutex = make(chan int)
	port.waitCondLockMutex = make(chan int, 1)
	port.waitReadersNum = 0
	port.settings = s

	// 4. Запускаем на выполнение отдельный поток, вычитывающий данные из COM-порта
	go C.ReadDataFromCOMPort(port.unknownData)
	return
}

// Ждать появления данных в порту
func (port *COMPort) WaitForData(timeOutMs int) error {
	// Увеличиваем количество ожидающих еще на одного
	port.waitCondLockMutex <- 1
	port.waitReadersNum++
	<-port.waitCondLockMutex
	// Ждем появление данных в порту, но не более timeOutMs мс
	select {
	case <-port.waitCondMutex:
	case <-time.After(time.Duration(timeOutMs) * time.Millisecond):
		port.waitCondLockMutex <- 1
		if port.waitReadersNum > 0 {
			port.waitReadersNum--
		} else {
			<-port.waitCondMutex
		}
		<-port.waitCondLockMutex
		return IOTimeOut
	}
	return NoError
}

// Открыть для работы COM-порт
func (port *COMPort) Close() error {
	// Блокируем доступ другим потокам
	waitTimeOut := port.GetSettings().IOTimeOutMs
	err := waitForMutex(port.rMutex, waitTimeOut)
	if err != nil {
		return err
	}
	err = waitForMutex(port.wMutex, waitTimeOut)
	if err != nil {
		return err
	}
	// Оповещаем всех ожидаюших, что данных больше не будет
	sendSignalForWaiters(port)

	// Закрываем порт, высвобождаем ресурсы
	return cerror2GoError(
		C.int(C.CloseCOMPort(port.unknownData)))
}

// Вычитать имеющиеся данные из порта, но не более len(p)
func (port *COMPort) Read(p []byte) (n int, err error) {
	// Блокируем доступ другим потокам
	defer func() {
		<-port.rMutex
	}()
	waitTimeOut := port.GetSettings().IOTimeOutMs
	err = waitForMutex(port.rMutex, waitTimeOut)
	if err != nil {
		return
	}

	n, err = port.rBuffer.Read(p)
	return
}

// Записать в порт данные
func (port *COMPort) Write(p []byte) (n int, err error) {
	// Блокируем доступ другим потокам
	defer func() {
		<-port.wMutex
	}()
	waitTimeOut := port.GetSettings().IOTimeOutMs
	err = waitForMutex(port.wMutex, waitTimeOut)
	if err != nil {
		return
	}
	// Используя, внешнюю C-функцию пишем данные в порт
	tmp := C.int(len(p))
	err = cerror2GoError(
		C.int(C.WriteDataToCOMPort(
			port.unknownData, unsafe.Pointer(&p[0]), &tmp)))
	n = int(tmp)
	return
}

// Реализация интерфейса ReadFrom
func (port *COMPort) WriteTo(w io.Writer) (n int64, err error) {
	// Блокируем доступ другим потокам
	defer func() {
		<-port.rMutex
	}()
	waitTimeOut := port.GetSettings().IOTimeOutMs
	err = waitForMutex(port.rMutex, waitTimeOut)
	if err != nil {
		return
	}

	n, err = port.rBuffer.WriteTo(w)
	return
}

// Реализация интерфейса WriteTo
func (port *COMPort) ReadFrom(r io.Reader) (n int64, err error) {
	// Блокируем доступ другим потокам
	defer func() {
		<-port.wMutex
	}()
	waitTimeOut := port.GetSettings().IOTimeOutMs
	err = waitForMutex(port.wMutex, waitTimeOut)
	if err != nil {
		return
	}

	p := make([]byte, 256)
	// Пока не получили EOF вычитываем данные из r
	// и пишем их в порт
	err = nil
	curReadBytes := 1
	for err == nil && curReadBytes > 0 {
		// Читаем данные из Reader'а
		curReadBytes, err = r.Read(p)
		// Если количество прочитанного > 0,
		if curReadBytes > 0 {
			// то пишем их в последовательный порт
			tmp := C.int(len(p))
			err2 := cerror2GoError(
				C.int(C.WriteDataToCOMPort(
					port.unknownData, unsafe.Pointer(&p[0]), &tmp)))
			n = n + int64(tmp)
			if err2 != NoError {
				return n, err2
			}
		}
	}
	return
}

// Вернуть настройки COM-порта
func (port *COMPort) GetSettings() *COMPortSettings {
	return port.settings
}
